summaryrefslogtreecommitdiffstats
path: root/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
diff options
context:
space:
mode:
Diffstat (limited to 'src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt')
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt456
1 files changed, 456 insertions, 0 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
new file mode 100644
index 000000000..b1d3c0040
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
@@ -0,0 +1,456 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import android.text.TextUtils
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.navigation.findNavController
+import androidx.navigation.fragment.navArgs
+import androidx.recyclerview.widget.GridLayoutManager
+import com.google.android.material.transition.MaterialSharedAxis
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.yuzu.yuzu_emu.HomeNavigationDirections
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.adapters.GamePropertiesAdapter
+import org.yuzu.yuzu_emu.databinding.FragmentGamePropertiesBinding
+import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.model.DriverViewModel
+import org.yuzu.yuzu_emu.model.GameProperty
+import org.yuzu.yuzu_emu.model.GamesViewModel
+import org.yuzu.yuzu_emu.model.HomeViewModel
+import org.yuzu.yuzu_emu.model.InstallableProperty
+import org.yuzu.yuzu_emu.model.SubmenuProperty
+import org.yuzu.yuzu_emu.model.TaskState
+import org.yuzu.yuzu_emu.utils.DirectoryInitialization
+import org.yuzu.yuzu_emu.utils.FileUtil
+import org.yuzu.yuzu_emu.utils.GameIconUtils
+import org.yuzu.yuzu_emu.utils.GpuDriverHelper
+import org.yuzu.yuzu_emu.utils.MemoryUtil
+import java.io.BufferedInputStream
+import java.io.BufferedOutputStream
+import java.io.File
+
+class GamePropertiesFragment : Fragment() {
+ private var _binding: FragmentGamePropertiesBinding? = null
+ private val binding get() = _binding!!
+
+ private val homeViewModel: HomeViewModel by activityViewModels()
+ private val gamesViewModel: GamesViewModel by activityViewModels()
+ private val driverViewModel: DriverViewModel by activityViewModels()
+
+ private val args by navArgs<GamePropertiesFragmentArgs>()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enterTransition = MaterialSharedAxis(MaterialSharedAxis.Y, true)
+ returnTransition = MaterialSharedAxis(MaterialSharedAxis.Y, false)
+ reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentGamePropertiesBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ // This is using the correct scope, lint is just acting up
+ @SuppressLint("UnsafeRepeatOnLifecycleDetector")
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ homeViewModel.setNavigationVisibility(visible = false, animated = true)
+ homeViewModel.setStatusBarShadeVisibility(true)
+
+ binding.buttonBack.setOnClickListener {
+ view.findNavController().popBackStack()
+ }
+
+ GameIconUtils.loadGameIcon(args.game, binding.imageGameScreen)
+ binding.title.text = args.game.title
+ binding.title.postDelayed(
+ {
+ binding.title.ellipsize = TextUtils.TruncateAt.MARQUEE
+ binding.title.isSelected = true
+ },
+ 3000
+ )
+
+ binding.buttonStart.setOnClickListener {
+ LaunchGameDialogFragment.newInstance(args.game)
+ .show(childFragmentManager, LaunchGameDialogFragment.TAG)
+ }
+
+ reloadList()
+
+ viewLifecycleOwner.lifecycleScope.apply {
+ launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ homeViewModel.openImportSaves.collect {
+ if (it) {
+ importSaves.launch(arrayOf("application/zip"))
+ homeViewModel.setOpenImportSaves(false)
+ }
+ }
+ }
+ }
+ launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ homeViewModel.reloadPropertiesList.collect {
+ if (it) {
+ reloadList()
+ homeViewModel.reloadPropertiesList(false)
+ }
+ }
+ }
+ }
+ }
+
+ setInsets()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ gamesViewModel.reloadGames(true)
+ }
+
+ private fun reloadList() {
+ _binding ?: return
+
+ driverViewModel.updateDriverNameForGame(args.game)
+ val properties = mutableListOf<GameProperty>().apply {
+ add(
+ SubmenuProperty(
+ R.string.info,
+ R.string.info_description,
+ R.drawable.ic_info_outline
+ ) {
+ val action = GamePropertiesFragmentDirections
+ .actionPerGamePropertiesFragmentToGameInfoFragment(args.game)
+ binding.root.findNavController().navigate(action)
+ }
+ )
+ add(
+ SubmenuProperty(
+ R.string.preferences_settings,
+ R.string.per_game_settings_description,
+ R.drawable.ic_settings
+ ) {
+ val action = HomeNavigationDirections.actionGlobalSettingsActivity(
+ args.game,
+ Settings.MenuTag.SECTION_ROOT
+ )
+ binding.root.findNavController().navigate(action)
+ }
+ )
+
+ if (GpuDriverHelper.supportsCustomDriverLoading()) {
+ add(
+ SubmenuProperty(
+ R.string.gpu_driver_manager,
+ R.string.install_gpu_driver_description,
+ R.drawable.ic_build,
+ detailsFlow = driverViewModel.selectedDriverTitle
+ ) {
+ val action = GamePropertiesFragmentDirections
+ .actionPerGamePropertiesFragmentToDriverManagerFragment(args.game)
+ binding.root.findNavController().navigate(action)
+ }
+ )
+ }
+
+ if (!args.game.isHomebrew) {
+ add(
+ SubmenuProperty(
+ R.string.add_ons,
+ R.string.add_ons_description,
+ R.drawable.ic_edit
+ ) {
+ val action = GamePropertiesFragmentDirections
+ .actionPerGamePropertiesFragmentToAddonsFragment(args.game)
+ binding.root.findNavController().navigate(action)
+ }
+ )
+ add(
+ InstallableProperty(
+ R.string.save_data,
+ R.string.save_data_description,
+ R.drawable.ic_save,
+ {
+ MessageDialogFragment.newInstance(
+ requireActivity(),
+ titleId = R.string.import_save_warning,
+ descriptionId = R.string.import_save_warning_description,
+ positiveAction = { homeViewModel.setOpenImportSaves(true) }
+ ).show(parentFragmentManager, MessageDialogFragment.TAG)
+ },
+ if (File(args.game.saveDir).exists()) {
+ { exportSaves.launch(args.game.saveZipName) }
+ } else {
+ null
+ }
+ )
+ )
+
+ val saveDirFile = File(args.game.saveDir)
+ if (saveDirFile.exists()) {
+ add(
+ SubmenuProperty(
+ R.string.delete_save_data,
+ R.string.delete_save_data_description,
+ R.drawable.ic_delete,
+ action = {
+ MessageDialogFragment.newInstance(
+ requireActivity(),
+ titleId = R.string.delete_save_data,
+ descriptionId = R.string.delete_save_data_warning_description,
+ positiveAction = {
+ File(args.game.saveDir).deleteRecursively()
+ Toast.makeText(
+ YuzuApplication.appContext,
+ R.string.save_data_deleted_successfully,
+ Toast.LENGTH_SHORT
+ ).show()
+ homeViewModel.reloadPropertiesList(true)
+ }
+ ).show(parentFragmentManager, MessageDialogFragment.TAG)
+ }
+ )
+ )
+ }
+
+ val shaderCacheDir = File(
+ DirectoryInitialization.userDirectory +
+ "/shader/" + args.game.settingsName.lowercase()
+ )
+ if (shaderCacheDir.exists()) {
+ add(
+ SubmenuProperty(
+ R.string.clear_shader_cache,
+ R.string.clear_shader_cache_description,
+ R.drawable.ic_delete,
+ {
+ if (shaderCacheDir.exists()) {
+ val bytes = shaderCacheDir.walkTopDown().filter { it.isFile }
+ .map { it.length() }.sum()
+ MemoryUtil.bytesToSizeUnit(bytes.toFloat())
+ } else {
+ MemoryUtil.bytesToSizeUnit(0f)
+ }
+ }
+ ) {
+ MessageDialogFragment.newInstance(
+ requireActivity(),
+ titleId = R.string.clear_shader_cache,
+ descriptionId = R.string.clear_shader_cache_warning_description,
+ positiveAction = {
+ shaderCacheDir.deleteRecursively()
+ Toast.makeText(
+ YuzuApplication.appContext,
+ R.string.cleared_shaders_successfully,
+ Toast.LENGTH_SHORT
+ ).show()
+ homeViewModel.reloadPropertiesList(true)
+ }
+ ).show(parentFragmentManager, MessageDialogFragment.TAG)
+ }
+ )
+ }
+ }
+ }
+ binding.listProperties.apply {
+ layoutManager =
+ GridLayoutManager(requireContext(), resources.getInteger(R.integer.grid_columns))
+ adapter = GamePropertiesAdapter(viewLifecycleOwner, properties)
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ driverViewModel.updateDriverNameForGame(args.game)
+ }
+
+ private fun setInsets() =
+ ViewCompat.setOnApplyWindowInsetsListener(
+ binding.root
+ ) { _: View, windowInsets: WindowInsetsCompat ->
+ val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
+
+ val leftInsets = barInsets.left + cutoutInsets.left
+ val rightInsets = barInsets.right + cutoutInsets.right
+
+ val smallLayout = resources.getBoolean(R.bool.small_layout)
+ if (smallLayout) {
+ val mlpListAll =
+ binding.listAll.layoutParams as ViewGroup.MarginLayoutParams
+ mlpListAll.leftMargin = leftInsets
+ mlpListAll.rightMargin = rightInsets
+ binding.listAll.layoutParams = mlpListAll
+ } else {
+ if (ViewCompat.getLayoutDirection(binding.root) ==
+ ViewCompat.LAYOUT_DIRECTION_LTR
+ ) {
+ val mlpListAll =
+ binding.listAll.layoutParams as ViewGroup.MarginLayoutParams
+ mlpListAll.rightMargin = rightInsets
+ binding.listAll.layoutParams = mlpListAll
+
+ val mlpIconLayout =
+ binding.iconLayout!!.layoutParams as ViewGroup.MarginLayoutParams
+ mlpIconLayout.topMargin = barInsets.top
+ mlpIconLayout.leftMargin = leftInsets
+ binding.iconLayout!!.layoutParams = mlpIconLayout
+ } else {
+ val mlpListAll =
+ binding.listAll.layoutParams as ViewGroup.MarginLayoutParams
+ mlpListAll.leftMargin = leftInsets
+ binding.listAll.layoutParams = mlpListAll
+
+ val mlpIconLayout =
+ binding.iconLayout!!.layoutParams as ViewGroup.MarginLayoutParams
+ mlpIconLayout.topMargin = barInsets.top
+ mlpIconLayout.rightMargin = rightInsets
+ binding.iconLayout!!.layoutParams = mlpIconLayout
+ }
+ }
+
+ val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab)
+ val mlpFab =
+ binding.buttonStart.layoutParams as ViewGroup.MarginLayoutParams
+ mlpFab.leftMargin = leftInsets + fabSpacing
+ mlpFab.rightMargin = rightInsets + fabSpacing
+ mlpFab.bottomMargin = barInsets.bottom + fabSpacing
+ binding.buttonStart.layoutParams = mlpFab
+
+ binding.layoutAll.updatePadding(
+ top = barInsets.top,
+ bottom = barInsets.bottom +
+ resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
+ )
+
+ windowInsets
+ }
+
+ private val importSaves =
+ registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
+ if (result == null) {
+ return@registerForActivityResult
+ }
+
+ val inputZip = requireContext().contentResolver.openInputStream(result)
+ val savesFolder = File(args.game.saveDir)
+ val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
+ cacheSaveDir.mkdir()
+
+ if (inputZip == null) {
+ Toast.makeText(
+ YuzuApplication.appContext,
+ getString(R.string.fatal_error),
+ Toast.LENGTH_LONG
+ ).show()
+ return@registerForActivityResult
+ }
+
+ IndeterminateProgressDialogFragment.newInstance(
+ requireActivity(),
+ R.string.save_files_importing,
+ false
+ ) {
+ try {
+ FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
+ val files = cacheSaveDir.listFiles()
+ var savesFolderFile: File? = null
+ if (files != null) {
+ val savesFolderName = args.game.programIdHex
+ for (file in files) {
+ if (file.isDirectory && file.name == savesFolderName) {
+ savesFolderFile = file
+ break
+ }
+ }
+ }
+
+ if (savesFolderFile != null) {
+ savesFolder.deleteRecursively()
+ savesFolder.mkdir()
+ savesFolderFile.copyRecursively(savesFolder)
+ savesFolderFile.deleteRecursively()
+ }
+
+ withContext(Dispatchers.Main) {
+ if (savesFolderFile == null) {
+ MessageDialogFragment.newInstance(
+ requireActivity(),
+ titleId = R.string.save_file_invalid_zip_structure,
+ descriptionId = R.string.save_file_invalid_zip_structure_description
+ ).show(parentFragmentManager, MessageDialogFragment.TAG)
+ return@withContext
+ }
+ Toast.makeText(
+ YuzuApplication.appContext,
+ getString(R.string.save_file_imported_success),
+ Toast.LENGTH_LONG
+ ).show()
+ homeViewModel.reloadPropertiesList(true)
+ }
+
+ cacheSaveDir.deleteRecursively()
+ } catch (e: Exception) {
+ Toast.makeText(
+ YuzuApplication.appContext,
+ getString(R.string.fatal_error),
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
+ }
+
+ /**
+ * Exports the save file located in the given folder path by creating a zip file and opening a
+ * file picker to save.
+ */
+ private val exportSaves = registerForActivityResult(
+ ActivityResultContracts.CreateDocument("application/zip")
+ ) { result ->
+ if (result == null) {
+ return@registerForActivityResult
+ }
+
+ IndeterminateProgressDialogFragment.newInstance(
+ requireActivity(),
+ R.string.save_files_exporting,
+ false
+ ) {
+ val saveLocation = args.game.saveDir
+ val zipResult = FileUtil.zipFromInternalStorage(
+ File(saveLocation),
+ saveLocation.replaceAfterLast("/", ""),
+ BufferedOutputStream(requireContext().contentResolver.openOutputStream(result))
+ )
+ return@newInstance when (zipResult) {
+ TaskState.Completed -> getString(R.string.export_success)
+ TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
+ }
+ }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
+ }
+}